18. Exercise: Dependency Injection
Expiration Manager
Your company Udacisearch runs lots of web crawls for its SEO product. Most of these crawls produce some kind of file output. You don't want to delete these files immediately (What if you need them later to review the results of a recent crawl?), but once one of these files has been around for more than 30 days without being touched, chances are you will never need it again.
You created a small program that checks whether a list of files has expired, or reached its 30-day limit. If any of the input files are expired, your program prints out their file names so you can delete them. In a real-world context, this technique is known as a TTL, or "Time to Live". TTLs are a common way for large systems to manage limited disk space and make sure it's not all being taken up by very old files that are never used. In the real world. In this example, the TTL is checked manually, but in many systems it happens as part of an automatic process.
OK, so you coded up this ExpirationChecker
class, but you ran into a problem: How are you going to unit test it? ExpirationChecker
depends on file metadata (last modified time) and the system time (to check whether the last modified time is expired, based on the current time), so how are you supposed to write a unit test? Do you just have old files sitting around on a test machine that can be used in test cases? No, that won't do — what if those files are accidentally deleted, or what if the expiration time changes?
Refactoring to Use Dependency Injection
After a while of thinking, you decide it's probably best to rewrite your ExpirationChecker
class to use dependency injection. Designing the code this way will allow you to inject real implementations of dependencies when running the program for real, and to use fake dependencies when running unit tests.
You can learn more about test fakes and other test doubles in the Java Application Deployment course. You don't need to know the details for this exercise, but suffice it to say they can make unit testing much easier!
For this exercise, you will use Guice, which is an open source dependency injection framework for Java.
Getting Started
For starters, you need to refactor the ExpirationChecker
class to be compatible with dependency injection.
First, annotate the constructor with the
@Inject
annotation.Create an instance field for a
java.time.Clock
and make theExpirationChecker
constructor take arguments for each of its instance fields. The body of the constructor should assign values to the instance fields.Now you need to change the code for
ExpirationChecker#isExpired()
to use the newClock
field instead ofInstant.now()
to get the current time. By doing this, you will be able to use a fakeClock
implementation in tests, to control whatExpirationChecker
thinks is the current time. (AFakeClock
has already been implemented for you, by the way.)Now, fill in
ExpirationCheckerModule.java
. The purpose of this module is to tell Guice (the dependency injection framework) which implementations it should use for each dependency yourExpirationChecker
needs to inject. The finalconfigure()
method should look like this:bind(Clock.class).toInstance(Clock.systemUTC());
bind(MetadataFetcher.class).to(MetadataFetcherImpl.class);This code tells Guice to use the "real" system clock and metadata fetcher for the
Clock
andMetadataFetcher
dependencies.Next, update
Main.java
to use Guice. You will have to create a new dependency injection container (which is called anInjector
in Guice terminology) that uses the module you just created:Guice.createInjector(new ExpirationCheckerModule())
, and then get an instance of theExpirationChecker
by usingInjector#getInstance(ExpirationChecker.class)
. This code should no longer be calling theExpirationChecker
constructor.Make sure
Main.java
compiles:javac Main.java
. Congratulations! You successfully used dependency injection to create an instance of your class.
Writing the Test
Now it's time to finish writing the unit test your class. Open up ExpirationCheckerTest.java
to get started. In the real world, you would be using a unit testing framework to do this, but to keep things simpler in this exercise, a "regular" main()
method will do just fine.
Find the code that instantiates the
ExpirationChecker
for the test. Change this code to use a GuiceInjector
to instantiate theExpirationChecker
instead. You'll have to configure this injector to use theFakeClock
andFakeMetadataFetcher
implementations. There is a terse way to do this in Guice using theModule
interface, which is compatible with lambdas:Injector injector = Guice.createInjector(
b -> b.bind(Clock.class).toInstance(fakeClock),
b -> b.bind(MetadataFetcher.class).toInstance(fakeMetadataFetcher));Notice how you were able to swap out the
ExpirationChecker
's dependencies with fake implementations very easily!Finally, run the test and make sure it passes.
javac *.java
java -ea ExpirationCheckerTestThe test should pass, which means you should not see anything printed to the command-line. You will know it failed if the test throws an
AssertionError
.
TODO List
Task Feedback:
Wow, that was a lot! You successfully refactored your code to use dependency injection, and in doing so, you made your code much easier to unit test!
That was just the tip of the iceberg when it comes to dependency injection, but hopefully this exercise familiarized you with the basics!
Code
If you need a code on the https://github.com/udacity.
export PATH=/data/jdk-15.0.1/bin:$PATH
export JAVA_HOME=/data/jdk-15.0.1/bin
export CLASSPATH="/home/workspace:/home/workspace/lib/*"